package db

import (
	"CSCredis/interface/server"
	"CSCredis/lock"
	"CSCredis/logger"
	"CSCredis/reply"
	"CSCredis/timewheel"
	"fmt"
	"runtime/debug"
	"strings"
	"sync"
	"time"
)

/**
 * @Author: hejing@szlabsun.com
 * @Description:
 * @File:  db
 * @Version: 1.0.0
 * @Date: 2021/7/24 11:45
 */

const (
	dataDictSize = 1 << 16
	ttlDictSize  = 1 << 10
	lockerSize   = 1024
)

// public DBImpl implements DB
type DBImpl struct {
	// key -> DataEntity
	data Dict
	// key -> expireTime (time.Time)
	ttlMap Dict
	// use this mutex for complicated command only, eg. rpush, incr ...
	locker *lock.Locks
	// stop all data access for execFlushDB 当要执行导出数据库之类的操作 则停止所有写方法
	stopWorld sync.WaitGroup
}
type DataEntity struct {
	Data interface{}
}
type ExecFunc func(db DB, args [][]byte) reply.Reply

// PreFunc analyses command line when queued command to `multi`
// returns related write keys and read keys
type PreFunc func(args [][]byte) ([]string, []string)

// CmdLine is alias for [][]byte, represents a command line
type CmdLine = [][]byte

// UndoFunc returns undo logs for the given command line
// execute from head to tail when undo
type UndoFunc func(db DB, args [][]byte) []CmdLine

// PutEntity a DataEntity into DB
func (db *DBImpl) PutEntity(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.Put(key, entity)
}

// Flush clean database
func (db *DBImpl) Flush() {
	db.stopWorld.Add(1)
	defer db.stopWorld.Done()

	db.data.Clear()
	db.ttlMap.Clear()
	db.locker = lock.Make(lockerSize)

}

func (db *DBImpl) GetTTLMap() Dict {
	return db.ttlMap
}

func (db *DBImpl) GetData() Dict {
	return db.data
}

// Expire sets ttlCmd of key
func (db *DBImpl) Expire(key string, expireTime time.Time) {
	db.stopWorld.Wait()
	db.ttlMap.Put(key, expireTime)
	taskKey := genExpireTask(key)
	/*
	 * 每个任务按照延长时间置入时间轮里的队列中(默认3600个槽)
	 * 如果任务的延长时间与3600取模，置于该槽的任务队列中，延长时间每大于3600一轮，则将其circle圈数加一
	 * 每秒触发一次定时任务，取出该时间节点下的任务队列
	 * 遍历任务队列，如果为当前循环(circle=0)的，则执行任务，不是当前循环的，则将其circle-1
	 */
	timewheel.At(expireTime, taskKey, func() {
		keys := []string{key}
		db.RWLocks(keys, nil)
		defer db.RWUnLocks(keys, nil)
		// check-lock-check, ttl may be updated during waiting lock
		logger.Info("expire " + key)
		rawExpireTime, ok := db.ttlMap.Get(key)
		if !ok {
			return
		}
		expireTime, _ := rawExpireTime.(time.Time)
		expired := time.Now().After(expireTime)
		if expired {
			db.Remove(key)
		}
	})
}

// RWLocks lock keys for writing and reading
func (db *DBImpl) RWLocks(writeKeys []string, readKeys []string) {
	db.locker.RWLocks(writeKeys, readKeys)
}

// RWUnLocks unlock keys for writing and reading
func (db *DBImpl) RWUnLocks(writeKeys []string, readKeys []string) {
	db.locker.RWUnLocks(writeKeys, readKeys)
}

// Persist cancel ttlCmd of key
func (db *DBImpl) Persist(key string) {
	db.stopWorld.Wait()
	db.ttlMap.Remove(key)
	taskKey := genExpireTask(key)
	timewheel.Cancel(taskKey)
}

func (db *DBImpl) GetAsString(key string) ([]byte, reply.Reply) {
	entity, ok := db.GetEntity(key)
	if !ok {
		return nil, nil
	}
	bytes, ok := entity.Data.([]byte)
	if !ok {
		return nil, &reply.UnknownErrReply{}
	}
	return bytes, nil
}

// GetEntity returns DataEntity bind to given key
func (db *DBImpl) GetEntity(key string) (*DataEntity, bool) {
	db.stopWorld.Wait()

	raw, ok := db.data.Get(key)
	if !ok {
		return nil, false
	}
	if db.IsExpired(key) {
		return nil, false
	}
	entity, _ := raw.(*DataEntity)
	return entity, true
}

// IsExpired check whether a key is expired
func (db *DBImpl) IsExpired(key string) bool {
	rawExpireTime, ok := db.ttlMap.Get(key)
	if !ok {
		return false
	}
	expireTime, _ := rawExpireTime.(time.Time)
	expired := time.Now().After(expireTime)
	if expired {
		db.Remove(key)
	}
	return expired
}

// Remove the given key from db
func (db *DBImpl) Remove(key string) {
	db.stopWorld.Wait()
	db.data.Remove(key)
	db.ttlMap.Remove(key)
	taskKey := genExpireTask(key)
	timewheel.Cancel(taskKey)
}

// Removes the given keys from db
func (db *DBImpl) Removes(keys ...string) (deleted int) {
	db.stopWorld.Wait()
	deleted = 0
	for _, key := range keys {
		_, exists := db.data.Get(key)
		if exists {
			db.Remove(key)
			deleted++
		}
	}
	return deleted
}

/* ---- TTL Functions ---- */
func genExpireTask(key string) string {
	return "expire:" + key
}

func (db *DBImpl) Exec(c server.Client, cmdLine [][]byte) (result reply.Reply) {
	defer func() {
		if err := recover(); err != nil {
			logger.Warn(fmt.Sprintf("error occurs: %v\n%s", err, string(debug.Stack())))
			result = &reply.UnknownErrReply{}
		}
	}()

	cmdName := strings.ToLower(string(cmdLine[0]))
	// authenticate
	if cmdName == "auth" {
		return Auth(c, cmdLine[1:])
	}
	//if !isAuthenticated(c) {
	//	return reply.MakeErrReply("NOAUTH Authentication required")
	//}

	// special commands
	//done := false
	//result, done = execSpecialCmd(c, cmdLine, cmdName, db)
	//if done {
	//	return result
	//}
	//if c != nil && c.InMultiState() {
	//	return enqueueCmd(db, c, cmdLine)
	//}

	// normal commands
	return execNormalCommand(db, cmdLine)
}

func execNormalCommand(db DB, cmdArgs [][]byte) reply.Reply {
	cmdName := strings.ToLower(string(cmdArgs[0]))
	cmd, ok := cmdTable[cmdName]
	if !ok {
		return reply.MakeErrReply("ERR unknown command '" + cmdName + "'")
	}
	if !validateArity(cmd.arity, cmdArgs) {
		return &reply.UnknownErrReply{}
	}

	//prepare := cmd.prepare
	//write, read := prepare(cmdArgs[1:])
	//db.RWLocks(write, read)
	//defer db.RWUnLocks(write, read)
	fun := cmd.executor
	return fun(db, cmdArgs[1:])
}

func validateArity(arity int, cmdArgs [][]byte) bool {
	argNum := len(cmdArgs)
	if arity >= 0 {
		return argNum == arity
	}
	return argNum >= -arity
}

// Auth validate client's password
func Auth(c server.Client, args [][]byte) reply.Reply {
	if len(args) != 1 {
		return reply.MakeErrReply("ERR wrong number of arguments for 'auth' command")
	}
	//if config.Properties.RequirePass == "" {
	//	return reply.MakeErrReply("ERR Client sent AUTH, but no password is set")
	//}
	passwd := string(args[0])
	c.SetPassword(passwd)
	//if global.CONFIG.Redis.Password != passwd {
	//	return reply.MakeErrReply("ERR invalid password")
	//}
	return &reply.OkReply{}
}

// PutIfAbsent insert an DataEntity only if the key not exists
func (db *DBImpl) PutIfAbsent(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.PutIfAbsent(key, entity)
}

// PutIfExists edit an existing DataEntity
func (db *DBImpl) PutIfExists(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.PutIfExists(key, entity)
}

func (db *DBImpl) AfterClientClose(c server.Client) {
	panic("implement me")
}

func (db *DBImpl) Close() {
	panic("implement me")
}

func NewDBImpl() *DBImpl {
	return &DBImpl{
		data:   MakeConcurrent(1 << 8),
		ttlMap: MakeConcurrent(1 << 8),
	}
}
